<!DOCTYPE html>
<meta charset=utf-8>
<title>Finishing an animation</title>
<link rel="help"
  href="https://drafts.csswg.org/web-animations/#finishing-an-animation-section">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>
<style>
.scroller {
  overflow: auto;
  height: 200px;
  width: 100px;
  will-change: transform;
}

.contents {
  height: 1000px;
  width: 100%;
}
</style>
<body>
<div id="log"></div>
<script>
'use strict';
  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.playbackRate = 0;

    assert_throws_dom('InvalidStateError', () => {
      animation.finish();
    });
  }, 'Finishing an animation with a zero playback rate throws');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    animation.effect.updateTiming({ iterations : Infinity });
    animation.play();
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();

    assert_throws_dom('InvalidStateError', () => {
      animation.finish();
    });
  }, 'Finishing an infinite animation throws');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame  which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.finish();

    assert_times_equal(animation.currentTime, 1000,
      'After finishing, the currentTime should be set to the end of the'
      + ' active duration');
  }, 'Finishing an animation seeks to the end time');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    // 1s past effect end
    animation.currentTime =
      animation.effect.getComputedTiming().endTime + 1;
    animation.finish();

    assert_time_equals_literal(animation.currentTime, 1000,
      'After finishing, the currentTime should be set back to the end of the'
      + ' active duration');
  }, 'Finishing an animation with a current time past the effect end jumps'
    + ' back to the end');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.currentTime = 1000;
    await animation.finished;

    animation.playbackRate = -1;
    animation.finish();

    assert_equals(animation.currentTime, 0,
                  'After finishing a reversed animation the currentTime ' +
                  'should be set to zero');
  }, 'Finishing a reversed animation jumps to zero time');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.currentTime = 1000;
    await animation.finished;

    animation.playbackRate = -1;
    animation.currentTime = -10000;
    animation.finish();

    assert_equals(animation.currentTime, 0,
                  'After finishing a reversed animation the currentTime ' +
                  'should be set back to zero');
  }, 'Finishing a reversed animation with a current time less than zero'
    + ' makes it jump back to zero');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.playbackRate = 0.5;
    animation.finish();

    assert_false(animation.pending);
    assert_equals(animation.playState, 'finished',
                  'The play state of a play-pending animation should become ' +
                  '"finished"');
    assert_times_equal(animation.startTime,
                      animation.timeline.currentTime - 1000 / 0.5,
                      'The start time of a play-pending animation should ' +
                      'be set');
  }, 'Finishing an animation while play-pending resolves the pending'
    + ' task immediately');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    const scroller = animation.timeline.scrollSource;
    // Make the scroll timeline inactive.
    scroller.style.overflow = 'visible';
    scroller.scrollTop;
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.finish();

    await animation.finished;

    assert_true(animation.pending);
    assert_equals(animation.playState, 'finished',
                  'The play state of a play-pending animation should become ' +
                  '"finished"');
    assert_times_equal(animation.currentTime, 1000,
                      'The current time of a play-pending animation should ' +
                      'be set to the end of the active duration');
    assert_equals(animation.startTime, null,
                      'The start time of a finished play-pending animation' +
                      ' should be be unresolved');
  }, 'Finishing an animation attached to inactive timeline while play-pending '
    + 'doesn\'t resolves the pending task');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    let resolvedFinished = false;
    animation.finished.then(() => {
      resolvedFinished = true;
    });

    await animation.ready;

    animation.finish();
    await Promise.resolve();

    assert_true(resolvedFinished, 'finished promise should be resolved');
  }, 'Finishing an animation resolves the finished promise synchronously');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    const promise = animation.ready;
    let readyResolved = false;

    animation.finish();
    animation.ready.then(() => { readyResolved = true; });

    const promiseResult = await animation.finished;
    await animation.ready;

    assert_equals(promiseResult, animation);
    assert_equals(animation.ready, promise);
    assert_true(readyResolved);
  }, 'A pending ready promise is resolved and not replaced when the animation'
    + ' is finished');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.effect.target.remove();

    const eventWatcher = new EventWatcher(t, animation, 'finish');

    await animation.ready;
    animation.finish();

    await eventWatcher.wait_for('finish');
    assert_equals(animation.effect.target.parentNode, null,
      'finish event should be fired for the animation on an orphaned element');
  }, 'Finishing an animation fires finish event on orphaned element');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    await animation.ready;

    const originalFinishPromise = animation.finished;

    animation.cancel();
    assert_equals(animation.startTime, null);
    assert_equals(animation.currentTime, null);

    const resolvedFinishPromise = animation.finished;
    assert_not_equals(originalFinishPromise, resolvedFinishPromise,
                'Canceling an animation should create a new finished promise');

    animation.finish();
    assert_equals(animation.playState, 'finished',
                  'The play state of a canceled animation should become ' +
                  '"finished"');
    assert_times_equal(animation.startTime,
                      animation.timeline.currentTime - 1000,
                      'The start time of a finished animation should be set');
    assert_times_equal(animation.currentTime, 1000,
                      'Hold time should be set to end boundary of the animation');

  }, 'Finishing a canceled animation sets the current and start times');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    const scroller = animation.timeline.scrollSource;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
    scroller.scrollTop = 0.25 * maxScroll;
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();

    const eventWatcher = new EventWatcher(t, animation, 'finish');
    animation.finish();
    await animation.finished;
    const finishEvent = await eventWatcher.wait_for('finish');
    assert_equals(animation.playState, 'finished',
      'Animation is finished.');
    assert_equals(animation.currentTime, 1000,
      'The current time is the end of the active duration in finished state.');
    assert_equals(animation.startTime, -750,
      'The start time is calculated to match the current time.');
    assert_equals(finishEvent.currentTime, 1000,
      'event.currentTime is the animation current time.');
    assert_equals(finishEvent.timelineTime, 250,
      'event.timelineTime is timeline.currentTime');
  }, 'Finishing idle animation produces correct state and fires finish event.');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    const scroller = animation.timeline.scrollSource;
    // Make the scroll timeline inactive.
    scroller.style.overflow = 'visible';
    await waitForNextFrame();
    assert_equals(animation.timeline.currentTime, null,
                  'Sanity check the timeline is inactive.');
    animation.finish();
    assert_equals(animation.playState, 'paused', 'Animation is paused.');
  }, 'Finishing idle animation attached to inactive timeline pauses the ' +
     'animation.');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    const scroller = animation.timeline.scrollSource;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;
    scroller.scrollTop = 0.25 * maxScroll;
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    await animation.ready;

    const eventWatcher = new EventWatcher(t, animation, 'finish');
    animation.finish();
    await animation.finished;
    const finishEvent = await eventWatcher.wait_for('finish');
    assert_equals(animation.playState, 'finished',
      'Animation is finished.');
    assert_equals(animation.currentTime, 1000,
      'The current time is the end of active duration in finished state.');
    assert_equals(animation.startTime, -750,
      'The start time is calculated to match animation current time.');
    assert_equals(finishEvent.currentTime, 1000,
      'event.currentTime is the animation current time.');
    assert_equals(finishEvent.timelineTime, 250,
      'event.timelineTime is timeline.currentTime');
  }, 'Finishing running animation produces correct state and fires finish event.');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    const scroller = animation.timeline.scrollSource;
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    await animation.ready;

    // Make the scroll timeline inactive.
    scroller.style.overflow = 'visible';
    scroller.scrollTop;
    await waitForNextFrame();
    assert_equals(animation.timeline.currentTime, null,
                  'Sanity check the timeline is inactive.');
    assert_equals(animation.playState, 'running',
                 'Sanity check the animation is running.');

    animation.finish();
    assert_equals(animation.playState, 'paused', 'Animation is paused.');
  }, 'Finishing running animation attached to inactive timeline pauses the ' +
     'animation.');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.pause();
    await animation.ready;

    animation.finish();

    assert_equals(animation.playState, 'finished',
                  'The play state of a paused animation should become ' +
                  '"finished"');
    assert_times_equal(animation.startTime,
                      animation.timeline.currentTime - 1000,
                      'The start time of a paused animation should be set');
  }, 'Finishing a paused animation resolves the start time');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    // Update playbackRate so we can test that the calculated startTime
    // respects it
    animation.playbackRate = 2;
    animation.pause();
    // While animation is still pause-pending call finish()
    animation.finish();

    assert_false(animation.pending);
    assert_equals(animation.playState, 'finished',
                  'The play state of a pause-pending animation should become ' +
                  '"finished"');
    assert_times_equal(animation.startTime,
                      animation.timeline.currentTime - 1000 / 2,
                      'The start time of a pause-pending animation should ' +
                      'be set');
  }, 'Finishing a pause-pending animation resolves the pending task'
    + ' immediately and update the start time');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.playbackRate = -2;
    animation.pause();
    animation.finish();

    assert_false(animation.pending);
    assert_equals(animation.playState, 'finished',
                  'The play state of a pause-pending animation should become ' +
                  '"finished"');
    assert_times_equal(animation.startTime, animation.timeline.currentTime,
                      'The start time of a pause-pending animation should be ' +
                      'set');
  }, 'Finishing a pause-pending animation with negative playback rate'
    + ' resolves the pending task immediately');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    await animation.ready;

    animation.pause();
    animation.play();
    // We are now in the unusual situation of being play-pending whilst having
    // a resolved start time. Check that finish() still triggers a transition
    // to the finished state immediately.
    animation.finish();

    assert_equals(animation.playState, 'finished',
                  'After aborting a pause then finishing an animation its play ' +
                  'state should become "finished" immediately');
  }, 'Finishing an animation during an aborted pause makes it finished'
    + ' immediately');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    await animation.ready;

    animation.updatePlaybackRate(2);
    assert_true(animation.pending);

    animation.finish();
    assert_false(animation.pending);
    assert_equals(animation.playbackRate, 2);
    assert_times_equal(animation.currentTime, 1000);
  }, 'A pending playback rate should be applied immediately when an animation'
    + ' is finished');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    await animation.ready;

    animation.updatePlaybackRate(0);

    assert_throws_dom('InvalidStateError', () => {
      animation.finish();
    });
  }, 'An exception should be thrown if the effective playback rate is zero');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    animation.effect.updateTiming({ iterations: Infinity });
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    animation.currentTime = 500;
    animation.playbackRate = -1;
    await animation.ready;

    animation.updatePlaybackRate(1);

    assert_throws_dom('InvalidStateError', () => {
      animation.finish();
    });
  }, 'An exception should be thrown when finishing if the effective playback rate'
    + ' is positive and the target effect end is infinity');

  promise_test(async t => {
    const animation = createScrollLinkedAnimation(t);
    animation.effect.updateTiming({ iterations: Infinity });
    // Wait for new animation frame which allows the timeline to compute new
    // current time.
    await waitForNextFrame();
    animation.play();
    await animation.ready;

    animation.updatePlaybackRate(-1);

    animation.finish();
    // Should not have thrown
  }, 'An exception is NOT thrown when finishing if the effective playback rate'
    + ' is negative and the target effect end is infinity');
</script>
</body>